-
Notifications
You must be signed in to change notification settings - Fork 165
feat: Add Call Recording feature + Sync with latest upstream #550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…ass hooks and internal WhatsApp classes
Feature/call recording fixes
New features from upstream: - Contact Blocked Verification - Enhanced Locked Chats - Disable Ads - Improved AntiRevoke with ConversationItemListener - ContactItemListener & MenuStatusListener refactoring - Various bug fixes and UI improvements Preserved fork feature: - Call Recording (Voice/Video as Audio)
- Merged upstream commits from 2c96d46 to 89416fe - Added version 2.26.2.XX support - Refactored FMessageWpp JID type detection - Improved HideSeen and HideReceipt privacy features - Refactored CustomToolbar and Unobfuscator - Fixed UI thread safety and activity lifecycle management - Added support for Android 16 (Baklava) - Preserved custom Call Recording feature - Updated changelog to reflect both upstream and fork changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds a full Call Recording feature (recording + playback/management UI) while syncing a large set of upstream changes.
Changes:
- Implemented Xposed-based call audio recording and saving to disk.
- Added in-app recordings browser + audio player dialog, and wired a new “Recordings” tab into the main UI.
- Updated resources/manifest/CI configuration to support the new feature and upstream updates.
Reviewed changes
Copilot reviewed 36 out of 38 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
gradlew |
Adds Gradle wrapper script to support consistent builds. |
changelog.txt |
Documents call recording + upstream sync items. |
app/src/main/res/xml/fragment_media.xml |
Adds call recording preferences (enable, path, settings entry). |
app/src/main/res/xml/file_paths.xml |
Adds FileProvider paths definition for sharing recordings. |
app/src/main/res/values/strings_recordings.xml |
Adds new strings for recordings UI + settings. |
app/src/main/res/values/strings.xml |
Adds call recording strings and a “video call screen recording” label/summary. |
app/src/main/res/values/arrays.xml |
Extends supported versions arrays. |
app/src/main/res/menu/bottom_nav_menu.xml |
Adds a bottom-nav item for “Recordings”. |
app/src/main/res/layout/item_recording.xml |
Defines a list item layout for a recording row. |
app/src/main/res/layout/fragment_recordings.xml |
Defines recordings list screen UI (selection bar, sort, empty view). |
app/src/main/res/layout/dialog_audio_player.xml |
Adds audio player dialog UI. |
app/src/main/res/layout/activity_call_recording_settings.xml |
Adds call recording mode selection UI (root vs non-root). |
app/src/main/res/drawable/ic_warning.xml |
New vector drawable used in settings UI. |
app/src/main/res/drawable/ic_recording.xml |
New vector drawable for recordings icon. |
app/src/main/res/drawable/ic_play.xml |
New vector drawable for audio playback. |
app/src/main/res/drawable/ic_pause.xml |
New vector drawable for audio playback. |
app/src/main/res/drawable/ic_close.xml |
New vector drawable for close actions. |
app/src/main/res/drawable/ic_check_circle.xml |
New vector drawable for settings advantages list. |
app/src/main/res/drawable/duration_badge_background.xml |
Adds shape background for duration badge. |
app/src/main/res/drawable/dialog_background.xml |
Adds background shape for audio dialog. |
app/src/main/res/drawable/circle_button_background.xml |
Adds circular background for play/pause button. |
app/src/main/java/com/wmods/wppenhacer/xposed/spoofer/HookBL.java |
Uses FeatureLoader.mApp instead of AndroidAppHelper.currentApplication(). |
app/src/main/java/com/wmods/wppenhacer/xposed/features/media/CallRecording.java |
Implements call recording hooks + audio capture + WAV writing. |
app/src/main/java/com/wmods/wppenhacer/xposed/features/general/Others.java |
Wraps sendAudioType in try/catch for safety. |
app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.java |
Adjusts OriginFMessageField discovery logic for broader matching. |
app/src/main/java/com/wmods/wppenhacer/xposed/core/FeatureLoader.java |
Registers CallRecording plugin for loading. |
app/src/main/java/com/wmods/wppenhacer/ui/fragments/RecordingsFragment.java |
Adds recordings browser UI + share/delete/actions. |
app/src/main/java/com/wmods/wppenhacer/ui/fragments/MediaFragment.java |
Wires preference click to open call recording settings activity. |
app/src/main/java/com/wmods/wppenhacer/ui/dialogs/AudioPlayerDialog.java |
Adds in-app audio playback dialog using MediaPlayer. |
app/src/main/java/com/wmods/wppenhacer/model/Recording.java |
Adds recording metadata model (duration, contact lookup, formatting). |
app/src/main/java/com/wmods/wppenhacer/adapter/RecordingsAdapter.java |
Adds adapter for recordings list + selection mode. |
app/src/main/java/com/wmods/wppenhacer/adapter/MainPagerAdapter.java |
Conditionally adds a 6th tab for recordings based on pref. |
app/src/main/java/com/wmods/wppenhacer/activities/MainActivity.java |
Hides recordings nav item when disabled; adds navigation to recordings tab. |
app/src/main/java/com/wmods/wppenhacer/activities/CallRecordingSettingsActivity.java |
Adds settings activity for recording mode + root check. |
app/src/main/java/com/wmods/wppenhacer/activities/AboutActivity.java |
Updates GitHub URL capitalization. |
app/src/main/AndroidManifest.xml |
Registers CallRecordingSettingsActivity and FileProvider. |
.gitignore |
Ignores key_base64.txt. |
.github/workflows/android.yml |
Expands CI branch triggers and improves secret handling for keystore decoding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <?xml version="1.0" encoding="utf-8"?> | ||
| <paths> | ||
| <external-path | ||
| name="external_files" | ||
| path="." /> |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This FileProvider paths resource is malformed: the <paths> root is missing xmlns:android, and the child entries should use android:name/android:path (not un-namespaced name/path). As-is, FileProvider will fail to parse this XML and sharing recordings will crash.
| for (String cmd : commands) { | ||
| try { | ||
| Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", cmd}); | ||
| int exitCode = process.waitFor(); | ||
| XposedBridge.log("WaEnhancer: " + cmd + " exit: " + exitCode); | ||
| } catch (Exception e) { | ||
| XposedBridge.log("WaEnhancer: Root failed: " + e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| permissionGranted = true; |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
permissionGranted is set to true regardless of whether the su/pm grant commands actually succeed (non-zero exit code or exception). That prevents retries and can leave root mode broken for the session; only mark it granted when the required commands succeed.
| for (String cmd : commands) { | |
| try { | |
| Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", cmd}); | |
| int exitCode = process.waitFor(); | |
| XposedBridge.log("WaEnhancer: " + cmd + " exit: " + exitCode); | |
| } catch (Exception e) { | |
| XposedBridge.log("WaEnhancer: Root failed: " + e.getMessage()); | |
| } | |
| } | |
| permissionGranted = true; | |
| boolean allSucceeded = true; | |
| for (String cmd : commands) { | |
| try { | |
| Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", cmd}); | |
| int exitCode = process.waitFor(); | |
| XposedBridge.log("WaEnhancer: " + cmd + " exit: " + exitCode); | |
| if (exitCode != 0) { | |
| allSucceeded = false; | |
| break; | |
| } | |
| } catch (Exception e) { | |
| XposedBridge.log("WaEnhancer: Root failed: " + e.getMessage()); | |
| allSucceeded = false; | |
| break; | |
| } | |
| } | |
| if (allSucceeded) { | |
| permissionGranted = true; | |
| } |
| import java.io.File; | ||
| import java.io.RandomAccessFile; | ||
| import java.io.IOException; | ||
| import java.lang.reflect.Field; |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
java.lang.reflect.Field is imported but not used in this class. Please remove unused imports to avoid lint warnings/failures.
| import java.lang.reflect.Field; |
| private void loadRecordings() { | ||
| allRecordings.clear(); | ||
|
|
||
| for (File baseDir : baseDirs) { | ||
| if (baseDir.exists() && baseDir.isDirectory()) { |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loadRecordings() performs recursive filesystem traversal and constructs Recording objects (disk I/O + contact lookups) on the main thread. On devices with many files this can cause jank/ANRs; please move the scan/metadata extraction to a background thread and post results back to the UI.
| } catch (IOException e) { | ||
| e.printStackTrace(); | ||
| dismiss(); | ||
| return; | ||
| } |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If setDataSource()/prepare() throws, this catch path calls dismiss() and returns before setOnDismissListener(...) is registered, so the partially-created MediaPlayer won’t be released. Ensure you clean up mediaPlayer (and handler callbacks) on this error path too (e.g., call releasePlayer() in the catch/finally).
| android:layout_width="36dp" | ||
| android:layout_height="36dp" | ||
| android:layout_alignParentEnd="true" | ||
| android:background="?selectableItemBackgroundBorderless" |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
android:background is set to ?selectableItemBackgroundBorderless here, but this project consistently uses ?attr/selectableItemBackgroundBorderless (and there is no local attr named selectableItemBackgroundBorderless). As written, the attribute reference may resolve to 0 and you’ll lose the expected touch feedback. Use ?attr/selectableItemBackgroundBorderless (or ?android:attr/selectableItemBackgroundBorderless).
| android:background="?selectableItemBackgroundBorderless" | |
| android:background="?attr/selectableItemBackgroundBorderless" |
| duration = (dataSize * 1000L) / byteRate; | ||
| } else if (sampleRate > 0) { | ||
| // Assume 16-bit mono | ||
| duration = (dataSize * 1000L) / (sampleRate * 2); |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential overflow in int multiplication before it is converted to long by use in a numeric context.
| duration = (dataSize * 1000L) / (sampleRate * 2); | |
| duration = (dataSize * 1000L) / (sampleRate * 2L); |
| DataOutputStream os = new DataOutputStream(process.getOutputStream()); | ||
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | ||
|
|
||
| os.writeBytes("id\n"); | ||
| os.writeBytes("exit\n"); | ||
| os.flush(); | ||
|
|
||
| // Read output | ||
| StringBuilder sb = new StringBuilder(); | ||
| String line; | ||
| while ((line = reader.readLine()) != null) { | ||
| sb.append(line); | ||
| } | ||
| rootOutput = sb.toString(); | ||
|
|
||
| int exitCode = process.waitFor(); | ||
| Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); | ||
|
|
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This InputStreamReader is not always closed on method exit.
| DataOutputStream os = new DataOutputStream(process.getOutputStream()); | |
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | |
| os.writeBytes("id\n"); | |
| os.writeBytes("exit\n"); | |
| os.flush(); | |
| // Read output | |
| StringBuilder sb = new StringBuilder(); | |
| String line; | |
| while ((line = reader.readLine()) != null) { | |
| sb.append(line); | |
| } | |
| rootOutput = sb.toString(); | |
| int exitCode = process.waitFor(); | |
| Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); | |
| try (DataOutputStream os = new DataOutputStream(process.getOutputStream()); | |
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { | |
| os.writeBytes("id\n"); | |
| os.writeBytes("exit\n"); | |
| os.flush(); | |
| // Read output | |
| StringBuilder sb = new StringBuilder(); | |
| String line; | |
| while ((line = reader.readLine()) != null) { | |
| sb.append(line); | |
| } | |
| rootOutput = sb.toString(); | |
| } | |
| int exitCode = process.waitFor(); | |
| Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); |
| DataOutputStream os = new DataOutputStream(process.getOutputStream()); | ||
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | ||
|
|
||
| os.writeBytes("id\n"); | ||
| os.writeBytes("exit\n"); | ||
| os.flush(); | ||
|
|
||
| // Read output | ||
| StringBuilder sb = new StringBuilder(); | ||
| String line; | ||
| while ((line = reader.readLine()) != null) { | ||
| sb.append(line); | ||
| } | ||
| rootOutput = sb.toString(); | ||
|
|
||
| int exitCode = process.waitFor(); | ||
| Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); | ||
|
|
||
| hasRoot = (exitCode == 0 && rootOutput.contains("uid=0")); |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This DataOutputStream is not always closed on method exit.
| DataOutputStream os = new DataOutputStream(process.getOutputStream()); | |
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | |
| os.writeBytes("id\n"); | |
| os.writeBytes("exit\n"); | |
| os.flush(); | |
| // Read output | |
| StringBuilder sb = new StringBuilder(); | |
| String line; | |
| while ((line = reader.readLine()) != null) { | |
| sb.append(line); | |
| } | |
| rootOutput = sb.toString(); | |
| int exitCode = process.waitFor(); | |
| Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); | |
| hasRoot = (exitCode == 0 && rootOutput.contains("uid=0")); | |
| try (DataOutputStream os = new DataOutputStream(process.getOutputStream()); | |
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { | |
| os.writeBytes("id\n"); | |
| os.writeBytes("exit\n"); | |
| os.flush(); | |
| // Read output | |
| StringBuilder sb = new StringBuilder(); | |
| String line; | |
| while ((line = reader.readLine()) != null) { | |
| sb.append(line); | |
| } | |
| rootOutput = sb.toString(); | |
| int exitCode = process.waitFor(); | |
| Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); | |
| hasRoot = (exitCode == 0 && rootOutput.contains("uid=0")); | |
| } |
| return recordings.size(); | ||
| } | ||
|
|
||
| static class ViewHolder extends RecyclerView.ViewHolder { |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ViewHolder has the same name as its supertype androidx.recyclerview.widget.RecyclerView$ViewHolder.
Summary
This PR adds a comprehensive Call Recording feature for WhatsApp while staying synchronized with the latest upstream changes.
What's New
🎙️ Call Recording Feature
🔄 Upstream Sync
This PR includes all upstream changes from commits
2c96d465to89416fe7(17 commits):Files Changed
New Files
CallRecordingSettingsActivity.java- Settings UI for call recordingCallRecording.java- Core call recording feature implementationRecordingsFragment.java- UI for browsing recordingsRecordingsAdapter.java- Adapter for recordings listAudioPlayerDialog.java- Audio playback dialogRecording.java- Model class for recording datastrings_recordings.xml- Localization stringsModified Files
changelog.txt- Updated with new features and upstream changesMainActivity.java&MainPagerAdapter.java- Added recordings tabMediaFragment.java- Enhanced media handlingAndroidManifest.xml- Added permissions and activity declarationsTesting
All custom features have been preserved while integrating upstream improvements. The merge was completed successfully with only minor changelog conflicts that were resolved.
Breaking Changes
None. All existing features remain intact.
Replaces
This PR replaces the previously submitted PRs:
Both have been closed in favor of this updated PR with the latest upstream sync.